import cv2
import numpy as np
import os
import csv
import math
from numpy.fft import fft, fftfreq
from scipy import signal
import matplotlib.pyplot as plt
import concurrent.futures
from functools import partial

# --- Matplotlib 中文和负号显示配置 ---
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

# --- 全局变量用于鼠标回调 ---
line_points = []
calibration_image_display = None

def mouse_callback(event, x, y, flags, param):
    """处理鼠标点击事件以进行标定"""
    global line_points, calibration_image_display
    
    if calibration_image_display is None:
        return

    if event == cv2.EVENT_LBUTTONDOWN:
        if len(line_points) < 2:
            line_points.append((x, y))
            cv2.circle(calibration_image_display, (x, y), 5, (0, 255, 0), -1)
            if len(line_points) == 2:
                cv2.line(calibration_image_display, line_points[0], line_points[1], (0, 255, 0), 2)
            cv2.imshow("标定窗口 - 请画一条线", calibration_image_display)

def get_pixel_to_mm_ratio(image_path):
    """通过用户交互获取像素到毫米的转换比例。"""
    global line_points, calibration_image_display
    line_points = []

    image = cv2.imread(image_path)
    if image is None:
        print(f"错误：无法读取用于标定的图像 {image_path}")
        return None

    calibration_image_display = image.copy()
    cv2.namedWindow("标定窗口 - 请画一条线")
    cv2.setMouseCallback("标定窗口 - 请画一条线", mouse_callback)

    print("\n--- 全局标定步骤 ---")
    print("1. 在弹出的 '标定窗口' 中，用鼠标左键点击两次，画一条你知道其实际长度的直线。")
    print("2. 按 'q' 键确认。")

    while len(line_points) < 2:
        cv2.imshow("标定窗口 - 请画一条线", calibration_image_display)
        if cv2.waitKey(20) & 0xFF == ord('q'):
            cv2.destroyAllWindows()
            print("标定被用户取消。")
            return None

    try:
        real_length_mm = float(input("请输入您所画直线的实际长度 (单位: 毫米)，然后按 Enter: "))
    except ValueError:
        print("输入无效，请输入一个数字。")
        cv2.destroyAllWindows()
        return None

    cv2.destroyAllWindows()

    p1, p2 = line_points
    pixel_distance = math.sqrt((p2[0] - p1[0])**2 + (p2[1] - p1[1])**2)

    if real_length_mm <= 0:
        print("实际长度必须大于0。")
        return None

    pixels_per_mm = pixel_distance / real_length_mm
    print(f"标定完成: 像素距离 = {pixel_distance:.2f} px, 实际长度 = {real_length_mm} mm")
    print(f"计算出的全局比例尺: {pixels_per_mm:.2f} 像素/毫米")
    
    return pixels_per_mm

def get_rod_center(image_path):
    """
    通过交互式阈值调整来获取棒的中心坐标。
    """
    image = cv2.imread(image_path)
    if image is None:
        print(f"错误：无法读取棒中心参考图像 {image_path}")
        return None

    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    blurred = cv2.GaussianBlur(gray, (5, 5), 0)
    
    initial_thresh, _ = cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
    
    cv2.namedWindow("棒中心阈值调整")
    cv2.createTrackbar("阈值", "棒中心阈值调整", int(initial_thresh), 255, lambda x: None)
    
    print("\n--- 棒中心定位步骤 ---")
    print("1. 在 '棒中心阈值调整' 窗口中，调整滑块直到能清晰看到棒的完整轮廓。")
    print("2. 按 'q' 键确认。")
    
    binary = None
    while True:
        thresh_value = cv2.getTrackbarPos("阈值", "棒中心阈值调整")
        _, binary = cv2.threshold(blurred, thresh_value, 255, cv2.THRESH_BINARY_INV)
        cv2.imshow("棒中心阈值调整", binary)
        if cv2.waitKey(20) & 0xFF == ord('q'):
            break
    
    cv2.destroyWindow("棒中心阈值调整")
    
    kernel = np.ones((5, 5), np.uint8)
    binary = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel, iterations=2)
    binary = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel, iterations=3)
    
    contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    
    if not contours:
        print("错误：在棒中心参考图中未能找到任何轮廓。")
        return None
        
    largest_contour = max(contours, key=cv2.contourArea)
    M = cv2.moments(largest_contour)
    
    if M["m00"] == 0:
        print("错误：无法计算棒中心的矩。")
        return None
        
    cX_float = M["m10"] / M["m00"]
    cY_float = M["m01"] / M["m00"]
    
    print(f"棒中心已定位，像素坐标: ({cX_float:.2f}, {cY_float:.2f})")
    return (cX_float, cY_float)


def analyze_and_plot_vibration(csv_path, pixels_per_mm, frame_rate, output_folder, rod_center_coords=None):
    """读取质心数据，进行全面的振动分析并绘图，同时保存数据到CSV文件。"""
    x_coords_pixel, y_coords_pixel = [], []
    
    try:
        with open(csv_path, 'r', encoding='utf-8') as csvfile:
            reader = csv.reader(csvfile)
            next(reader) # 跳过表头
            for row in reader:
                x_coords_pixel.append(float(row[1]))
                y_coords_pixel.append(float(row[2]))
    except (StopIteration, FileNotFoundError):
        print(f"  - 警告: 无法读取或找不到CSV文件 {csv_path}。跳过分析。")
        return
    except Exception as e:
        print(f"  - 错误: 读取CSV文件时出错: {e}。跳过分析。")
        return

    if not y_coords_pixel:
        print(f"  - 警告: {csv_path} 中无数据。跳过分析。")
        return

    x_coords_pixel = np.array(x_coords_pixel)
    y_coords_pixel = np.array(y_coords_pixel)

    # 计算相对于平均位置的位移
    disp_x_mm = (x_coords_pixel - np.mean(x_coords_pixel)) / pixels_per_mm
    disp_y_mm = (y_coords_pixel - np.mean(y_coords_pixel)) / pixels_per_mm
    
    N = len(disp_y_mm)
    T = 1.0 / frame_rate
    time = np.linspace(0.0, N * T, N, endpoint=False)
    
    if rod_center_coords is not None:
        rod_center_x_px, rod_center_y_px = rod_center_coords
        dev_x_px = x_coords_pixel - rod_center_x_px
        dev_y_px = y_coords_pixel - rod_center_y_px
        dev_x_mm = dev_x_px / pixels_per_mm
        dev_y_mm = dev_y_px / pixels_per_mm

        deviation_csv_path = os.path.join(output_folder, 'deviation_from_center_data.csv')
        with open(deviation_csv_path, 'w', newline='', encoding='utf-8') as csvfile:
            writer = csv.writer(csvfile)
            writer.writerow(['时间(s)', 'X方向偏离(mm)', 'Y方向偏离(mm)'])
            for t, x, y in zip(time, dev_x_mm, dev_y_mm):
                writer.writerow([t, x, y])
        print(f"  - 形心偏离中心位置数据已保存到: {deviation_csv_path}")

        plt.figure(figsize=(8, 8))
        plt.plot(dev_x_mm, dev_y_mm, linewidth=0.5, label='质心轨迹')
        plt.plot(0, 0, 'r+', markersize=12, markeredgewidth=2, label='棒中心')
        plt.title('形心相对棒中心运动轨迹')
        plt.xlabel('X方向偏离 (mm)')
        plt.ylabel('Y方向偏离 (mm)')
        plt.grid(True)
        plt.axis('equal')
        plt.legend()
        plt.savefig(os.path.join(output_folder, 'centroid_deviation_trajectory.png'))
        plt.close()

    displacement_csv_path = os.path.join(output_folder, 'displacement_data.csv')
    with open(displacement_csv_path, 'w', newline='', encoding='utf-8') as csvfile:
        writer = csv.writer(csvfile)
        writer.writerow(['时间(s)', 'X方向位移(mm)', 'Y方向位移(mm)'])
        for t, x, y in zip(time, disp_x_mm, disp_y_mm):
            writer.writerow([t, x, y])
    print(f"  - 位移数据已保存到: {displacement_csv_path}")
    
    plt.figure(figsize=(8, 8))
    plt.plot(disp_x_mm, disp_y_mm, linewidth=0.5)
    plt.title('质心运动轨迹 (相对平均位置)')
    plt.xlabel('X方向位移 (mm)')
    plt.ylabel('Y方向位移 (mm)')
    plt.grid(True)
    plt.axis('equal')
    plt.savefig(os.path.join(output_folder, 'centroid_trajectory.png'))
    plt.close()

    fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 10), sharex=True)
    ax1.plot(time, disp_y_mm)
    ax1.set_title('Y方向振动位移-时间图')
    ax1.set_ylabel('位移 (mm)')
    ax1.grid(True)
    ax2.plot(time, disp_x_mm)
    ax2.set_title('X方向振动位移-时间图')
    ax2.set_ylabel('位移 (mm)')
    ax2.set_xlabel('时间 (s)')
    ax2.grid(True)
    plt.savefig(os.path.join(output_folder, 'displacement_vs_time_XY.png'))
    plt.close()

    def plot_fft_and_find_peaks(disp_data, axis_name):
        yf = fft(disp_data.astype(np.float64))
        xf = fftfreq(N, T)[:N//2]
        amplitude = 2.0/N * np.abs(yf[0:N//2])

        plt.figure(figsize=(12, 6))
        plt.plot(xf.tolist(), amplitude.tolist())
        plt.title(f'{axis_name}方向频域分析 (FFT)')
        plt.xlabel('频率 (Hz)')
        plt.ylabel('振幅 (mm)')
        plt.grid(True)
        
        peaks, _ = signal.find_peaks(amplitude, prominence=np.max(amplitude)/20, height=np.max(amplitude)/10)
        top_peaks_indices = sorted(peaks, key=lambda p: amplitude[p].item(), reverse=True)[:5]
        
        with open(os.path.join(output_folder, 'frequency_results.txt'), 'a', encoding='utf-8') as f:
            f.write(f"\n--- {axis_name}方向前五阶固有频率 ---\n")
            if not top_peaks_indices:
                f.write("未能找到显著的频率峰值。\n")
            else:
                for i, peak_idx in enumerate(top_peaks_indices):
                    peak_freq = xf[peak_idx]
                    peak_amp = amplitude[peak_idx]
                    result_line = f"  第 {i+1} 阶频率: {peak_freq:.2f} Hz (振幅: {peak_amp:.4f} mm)\n"
                    f.write(result_line)
                    plt.axvline(float(peak_freq), color='r', linestyle='--', alpha=0.7)
                    plt.text(float(peak_freq), float(amplitude[peak_idx]), f' {peak_freq:.2f} Hz', color='red')
        
        fft_csv_path = os.path.join(output_folder, f'fft_data_{axis_name}.csv')
        with open(fft_csv_path, 'w', newline='', encoding='utf-8') as csvfile:
            writer = csv.writer(csvfile)
            writer.writerow(['频率(Hz)', '振幅(mm)'])
            for freq, amp in zip(xf, amplitude):
                writer.writerow([freq, amp])
        print(f"  - {axis_name}方向FFT数据已保存到: {fft_csv_path}")
        
        plt.savefig(os.path.join(output_folder, f'frequency_analysis_{axis_name}.png'))
        plt.close()

    def plot_psd(disp_data, axis_name):
        freqs, psd = signal.welch(disp_data, fs=frame_rate, nperseg=1024)
        plt.figure(figsize=(12, 6))
        plt.semilogy(freqs, psd)
        plt.title(f'{axis_name}方向功率谱密度 (PSD)')
        plt.xlabel('频率 (Hz)')
        plt.ylabel('功率/频率 (dB/Hz)')
        plt.grid(True)
        
        psd_csv_path = os.path.join(output_folder, f'psd_data_{axis_name}.csv')
        with open(psd_csv_path, 'w', newline='', encoding='utf-8') as csvfile:
            writer = csv.writer(csvfile)
            writer.writerow(['频率(Hz)', '功率/频率(dB/Hz)'])
            for freq, p in zip(freqs, psd):
                writer.writerow([freq, p])
        print(f"  - {axis_name}方向PSD数据已保存到: {psd_csv_path}")
        
        plt.savefig(os.path.join(output_folder, f'psd_analysis_{axis_name}.png'))
        plt.close()

    freq_results_path = os.path.join(output_folder, 'frequency_results.txt')
    if os.path.exists(freq_results_path):
        os.remove(freq_results_path)

    plot_fft_and_find_peaks(disp_y_mm, 'Y')
    plot_fft_and_find_peaks(disp_x_mm, 'X')
    plot_psd(disp_y_mm, 'Y')
    plot_psd(disp_x_mm, 'X')


def get_initial_position_from_template(template_image):
    """
    使用鲁棒的整数像素方法从模板图像中获取棒的初始位置和宽度。
    """
    height, width = template_image.shape
    one_third_width = width // 3
    left_roi = template_image[:, 0:one_third_width]
    right_roi = template_image[:, (2 * one_third_width):width]

    _, left_thresh = cv2.threshold(left_roi, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
    left_contours, _ = cv2.findContours(left_thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
    
    rod_left_x = -1
    if left_contours:
        all_left_points = np.vstack([cnt for cnt in left_contours])
        rod_left_x = all_left_points[:, 0, 0].min()

    _, right_thresh = cv2.threshold(right_roi, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)
    right_contours, _ = cv2.findContours(right_thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    rod_right_x = -1
    if right_contours:
        all_right_points = np.vstack([cnt for cnt in right_contours])
        rod_right_x = all_right_points[:, 0, 0].max() + (2 * one_third_width)

    if rod_left_x == -1 or rod_right_x == -1 or rod_left_x >= rod_right_x:
        return None, None, None

    initial_x = (rod_left_x + rod_right_x) / 2.0
    initial_y = height / 2.0
    initial_width = rod_right_x - rod_left_x
    
    return initial_x, initial_y, initial_width


def process_single_image_worker(task_info):
    """
    【新算法核心】处理单个图像，计算其相对于模板的亚像素位移。
    """
    filename = task_info['filename']
    input_folder = task_info['input_folder']
    template_roi = task_info['template_roi']
    initial_x = task_info['initial_x']
    initial_y = task_info['initial_y']
    
    file_path = os.path.join(input_folder, filename)
    current_image = cv2.imread(file_path, cv2.IMREAD_GRAYSCALE)
    if current_image is None:
        return None

    # 定义当前图像中与模板ROI相同大小和位置的ROI
    h, w = template_roi.shape
    current_roi = current_image[0:h, 0:w]

    # 使用相位相关计算亚像素位移
    # 我们需要处理浮点数图像
    shift, _ = cv2.phaseCorrelate(np.float32(template_roi), np.float32(current_roi))
    dx, dy = shift

    # OpenCV的phaseCorrelate返回的是将第二个图像移动到第一个图像的位移。
    # 如果杆在当前图像中向右移动了d像素，则质心增加了d。
    # 这意味着要将当前图像移动到模板，需要向左移动d像素，即dx = -d。
    # 因此，新的质心 = 初始质心 - dx。
    cX_float = initial_x - dx
    cY_float = initial_y - dy
    
    return (filename, cX_float, cY_float)


def process_images_in_folder(input_folder, output_folder, num_images_to_save):
    """
    【新算法 - V10 - 相位相关】
    使用相位相关来跟踪整个图像的亚像素位移。
    """
    try:
        file_list = sorted([f for f in os.listdir(input_folder) if f.lower().endswith((".bmp", ".png", ".jpg", ".jpeg"))])
        if not file_list:
            print(f"  - 警告: 文件夹 {input_folder} 中没有找到图像文件。")
            return None, None
    except FileNotFoundError:
        print(f"  - 错误: 找不到文件夹 {input_folder}。")
        return None, None

    total_files = len(file_list)
    if total_files == 0:
        return None, None

    # 1. 使用第一张图像作为参考模板
    template_path = os.path.join(input_folder, file_list[0])
    template_image = cv2.imread(template_path, cv2.IMREAD_GRAYSCALE)
    if template_image is None:
        print(f"  - 错误: 无法读取模板图像 {template_path}")
        return None, None

    # 2. 从模板中获取初始位置 (使用鲁棒的整数像素方法)
    initial_x, initial_y, initial_width = get_initial_position_from_template(template_image)
    if initial_x is None:
        print(f"  - 警告: 无法从模板图像中确定初始杆位置。")
        return None, None

    # 定义一个覆盖整个棒的稳定ROI作为相位相关的模板
    # 我们使用整个图像，因为相位相关对此很鲁棒
    template_roi = template_image
    
    # 第一个图像的结果是初始位置，位移为(0,0)
    height = template_image.shape[0]
    results = [(file_list[0], initial_x, initial_y, initial_width * height)]

    # 准备并行处理任务 (从第二张图像开始)
    tasks = []
    for filename in file_list[1:]:
        tasks.append({
            'filename': filename,
            'input_folder': input_folder,
            'template_roi': template_roi,
            'initial_x': initial_x,
            'initial_y': initial_y
        })
    
    processed_results = []
    try:
        from tqdm import tqdm
        with concurrent.futures.ThreadPoolExecutor(max_workers=os.cpu_count()) as executor:
            # 将结果添加到processed_results列表中
            processed_results.extend(list(tqdm(executor.map(process_single_image_worker, tasks), total=len(tasks), desc=f"处理 {os.path.basename(input_folder)}")))
    except ImportError:
        print(f"正在处理文件夹: {input_folder} (共 {total_files} 张图像)...")
        with concurrent.futures.ThreadPoolExecutor(max_workers=os.cpu_count()) as executor:
            processed_results.extend(list(executor.map(process_single_image_worker, tasks)))
        print("处理完成。")

    # 过滤掉失败的结果并将它们与第一个结果合并
    valid_results = [res for res in processed_results if res is not None]
    # 将初始结果添加到列表的开头
    final_results = [results[0]] + [(res[0], res[1], res[2], initial_width * height) for res in valid_results]
    
    if len(final_results) < total_files * 0.9: # 如果超过10%的帧失败，则发出警告
         print(f"  - 警告: {total_files - len(final_results)} / {total_files} 帧处理失败。")

    if not final_results:
        print(f"  - 警告: 未能从 {input_folder} 的任何图像中提取质心。")
        return None, None

    # 保存质心数据
    centroids_data = [(res[0], res[1], res[2]) for res in final_results]
    csv_path = os.path.join(output_folder, 'centroids.csv')
    try:
        with open(csv_path, 'w', newline='', encoding='utf-8') as csvfile:
            writer = csv.writer(csvfile)
            writer.writerow(['文件名', '质心_X', '质心_Y'])
            writer.writerows(centroids_data)
        print(f"  - 质心数据已保存到: {csv_path}")
    except IOError as e:
        print(f"  - 错误: 无法写入CSV文件 {csv_path}: {e}")
        return None, None
    
    # 对于面积异常检测，我们假设宽度恒定
    # 注意：这个逻辑现在只是一个形式，因为我们不再为每帧计算宽度
    abnormal_info = None

    return csv_path, abnormal_info


# --- 主程序 ---
if __name__ == "__main__":
    root_input_dir = input("请输入包含所有实验工况的根文件夹地址: ")
    if not os.path.isdir(root_input_dir):
        print(f"错误: 文件夹 '{root_input_dir}' 不存在。")
        exit()

    image_extensions = {'.bmp', '.png', '.jpg', '.jpeg'}

    # 1. 进行一次性全局标定
    biaoding_folder_path = os.path.join(root_input_dir, 'biaoding')
    if not os.path.isdir(biaoding_folder_path):
        print(f"错误: 未在根目录中找到 'biaoding' 文件夹。请确保该文件夹存在并包含标定图片。")
        exit()
    
    calibration_image_name = next((f for f in sorted(os.listdir(biaoding_folder_path)) if os.path.splitext(f)[1].lower() in image_extensions), None)
    if not calibration_image_name:
        print("错误：在 'biaoding' 文件夹中找不到任何图像用于标定。")
        exit()
    
    calibration_image_path = os.path.join(biaoding_folder_path, calibration_image_name)
    print(f"使用标定图像: {calibration_image_path}")
    pixels_per_mm = get_pixel_to_mm_ratio(calibration_image_path)

    if pixels_per_mm is None:
        print("标定失败，程序退出。")
        exit()

    # 2. 定位棒的初始中心
    rod_center_coords = None
    bangzhongxin_folder_path = os.path.join(root_input_dir, 'bangzhongxin')
    if not os.path.isdir(bangzhongxin_folder_path):
        print(f"\n警告: 未在根目录中找到 'bangzhongxin' 文件夹。将无法计算形心偏离中心的距离。")
    else:
        rod_center_image_name = next((f for f in sorted(os.listdir(bangzhongxin_folder_path)) if os.path.splitext(f)[1].lower() in image_extensions), None)
        if not rod_center_image_name:
            print("警告：在 'bangzhongxin' 文件夹中找不到任何图像用于定位棒中心。")
        else:
            rod_center_image_path = os.path.join(bangzhongxin_folder_path, rod_center_image_name)
            print(f"使用棒中心参考图像: {rod_center_image_path}")
            rod_center_coords = get_rod_center(rod_center_image_path)
            if rod_center_coords is None:
                print("警告：未能成功定位棒中心，将无法计算偏离距离。")


    # 3. 查找所有包含图像文件的 "叶子" 实验文件夹
    experiment_folders = []
    for dirpath, dirnames, filenames in os.walk(root_input_dir):
        if os.path.basename(dirpath) in ['biaoding', 'bangzhongxin']:
            dirnames[:] = [] 
            continue

        if any(os.path.splitext(f)[1].lower() in image_extensions for f in filenames):
            experiment_folders.append(dirpath)

    leaf_folders = [f for f in experiment_folders if not any(f != g and g.startswith(f) for g in experiment_folders)]


    if not leaf_folders:
        print(f"在 '{root_input_dir}' 及其子文件夹中未找到任何有效的实验文件夹。")
        exit()
    
    print(f"\n找到了 {len(leaf_folders)} 个实验文件夹，准备处理...")
    for folder in leaf_folders:
        print(f"  - {os.path.relpath(folder, root_input_dir)}")


    # 4. 创建总输出目录
    root_output_dir = os.path.join(os.path.dirname(root_input_dir.rstrip('/\\')), os.path.basename(root_input_dir.rstrip('/\\')) + "_processed")
    os.makedirs(root_output_dir, exist_ok=True)
    print(f"\n所有结果将保存在: {root_output_dir}")

    # 5. 循环处理每个实验文件夹
    FRAME_RATE = 1000 
    NUM_IMAGES_TO_SAVE = 10
    
    abnormal_folders_summary = []

    for i, input_folder in enumerate(leaf_folders):
        relative_path = os.path.relpath(input_folder, root_input_dir)
        output_folder = os.path.join(root_output_dir, relative_path)
        os.makedirs(output_folder, exist_ok=True)

        print(f"\n--- [{i+1}/{len(leaf_folders)}] 开始处理实验: {relative_path} ---")
        
        # 注意：不再需要num_images_to_save，因为我们不再为每帧绘制标记
        centroids_csv_file, abnormal_info = process_images_in_folder(input_folder, output_folder, NUM_IMAGES_TO_SAVE)
        
        if abnormal_info:
            abnormal_folders_summary.append(abnormal_info)

        if centroids_csv_file:
            print(f"  - 质心数据已保存，开始振动分析...")
            analyze_and_plot_vibration(centroids_csv_file, pixels_per_mm, FRAME_RATE, output_folder, rod_center_coords)
            print(f"  - 分析完成，结果保存在: {output_folder}")
        else:
            print(f"  - 警告: 未能生成质心数据CSV文件，跳过此文件夹的分析。")

    print("\n\n所有实验工况均已处理完毕！")

    if abnormal_folders_summary:
        print("\n--- 轮廓面积异常报告 ---")
        for folder_info in abnormal_folders_summary:
            print(f"\n文件夹: {folder_info['folder']}")
            print("  以下图像的轮廓面积被判定为异常:")
            for img_info in folder_info['abnormal_images']:
                print(f"    - 文件名: {img_info['filename']}, 面积: {img_info['area']:.2f}, 正常范围: [{img_info['expected_range'][0]:.2f}, {img_info['expected_range'][1]:.2f}]")
        print("\n请检查上述文件夹中的图像，以确认识别出的轮廓是否正确。")
    else:
        print("\n所有文件夹的轮廓面积均在正常范围内，未检测到异常。")
